Implement the [patch] section of the manifest
authorAlex Crichton <alex@alexcrichton.com>
Fri, 12 May 2017 18:07:46 +0000 (11:07 -0700)
committerAlex Crichton <alex@alexcrichton.com>
Tue, 18 Jul 2017 16:45:30 +0000 (09:45 -0700)
This is an implementation of [RFC 1969] which adds a new section to top-level
manifests: `[patch]`. This section allows you to patch existing sources with
new versions of crates, possibly replacing the versions that already exist in
the source. More details about this feature can be found in the RFC itself.

[RFC 1969]: https://github.com/rust-lang/rfcs/pull/1969

14 files changed:
src/cargo/core/dependency.rs
src/cargo/core/manifest.rs
src/cargo/core/registry.rs
src/cargo/core/resolver/encode.rs
src/cargo/core/resolver/mod.rs
src/cargo/core/workspace.rs
src/cargo/ops/lockfile.rs
src/cargo/ops/registry.rs
src/cargo/ops/resolve.rs
src/cargo/util/toml/mod.rs
src/doc/manifest.md
src/doc/specifying-dependencies.md
tests/cargotest/support/mod.rs
tests/patch.rs [new file with mode: 0644]

index c748ee9c51189259c79ca9513ef51cd42ff58d84..2d8b4d038539ecc2bb8cfee07f131e5ba8825e03 100644 (file)
@@ -258,6 +258,12 @@ impl Dependency {
             .set_source_id(id.source_id().clone())
     }
 
+    /// Returns whether this is a "locked" dependency, basically whether it has
+    /// an exact version req.
+    pub fn is_locked(&self) -> bool {
+        // Kind of a hack to figure this out, but it works!
+        self.inner.req.to_string().starts_with("=")
+    }
 
     /// Returns false if the dependency is only used to build the local package.
     pub fn is_transitive(&self) -> bool {
@@ -292,6 +298,12 @@ impl Dependency {
         self.matches_id(sum.package_id())
     }
 
+    /// Returns true if the package (`sum`) can fulfill this dependency request.
+    pub fn matches_ignoring_source(&self, sum: &Summary) -> bool {
+        self.name() == sum.package_id().name() &&
+            self.version_req().matches(sum.package_id().version())
+    }
+
     /// Returns true if the package (`id`) can fulfill this dependency request.
     pub fn matches_id(&self, id: &PackageId) -> bool {
         self.inner.name == id.name() &&
index 14087106492020e3003f49b05767572440183fca..662a5cbe4ce71721e697e0a6b8ada589a4d99557 100644 (file)
@@ -5,6 +5,7 @@ use std::rc::Rc;
 
 use semver::Version;
 use serde::ser;
+use url::Url;
 
 use core::{Dependency, PackageId, Summary, SourceId, PackageIdSpec};
 use core::WorkspaceConfig;
@@ -28,6 +29,7 @@ pub struct Manifest {
     profiles: Profiles,
     publish: bool,
     replace: Vec<(PackageIdSpec, Dependency)>,
+    patch: HashMap<Url, Vec<Dependency>>,
     workspace: WorkspaceConfig,
     original: Rc<TomlManifest>,
 }
@@ -35,6 +37,7 @@ pub struct Manifest {
 #[derive(Clone, Debug)]
 pub struct VirtualManifest {
     replace: Vec<(PackageIdSpec, Dependency)>,
+    patch: HashMap<Url, Vec<Dependency>>,
     workspace: WorkspaceConfig,
     profiles: Profiles,
 }
@@ -225,6 +228,7 @@ impl Manifest {
                profiles: Profiles,
                publish: bool,
                replace: Vec<(PackageIdSpec, Dependency)>,
+               patch: HashMap<Url, Vec<Dependency>>,
                workspace: WorkspaceConfig,
                original: Rc<TomlManifest>) -> Manifest {
         Manifest {
@@ -238,6 +242,7 @@ impl Manifest {
             profiles: profiles,
             publish: publish,
             replace: replace,
+            patch: patch,
             workspace: workspace,
             original: original,
         }
@@ -257,6 +262,7 @@ impl Manifest {
     pub fn publish(&self) -> bool { self.publish }
     pub fn replace(&self) -> &[(PackageIdSpec, Dependency)] { &self.replace }
     pub fn original(&self) -> &TomlManifest { &self.original }
+    pub fn patch(&self) -> &HashMap<Url, Vec<Dependency>> { &self.patch }
     pub fn links(&self) -> Option<&str> {
         self.links.as_ref().map(|s| &s[..])
     }
@@ -284,10 +290,12 @@ impl Manifest {
 
 impl VirtualManifest {
     pub fn new(replace: Vec<(PackageIdSpec, Dependency)>,
+               patch: HashMap<Url, Vec<Dependency>>,
                workspace: WorkspaceConfig,
                profiles: Profiles) -> VirtualManifest {
         VirtualManifest {
             replace: replace,
+            patch: patch,
             workspace: workspace,
             profiles: profiles,
         }
@@ -297,6 +305,10 @@ impl VirtualManifest {
         &self.replace
     }
 
+    pub fn patch(&self) -> &HashMap<Url, Vec<Dependency>> {
+        &self.patch
+    }
+
     pub fn workspace_config(&self) -> &WorkspaceConfig {
         &self.workspace
     }
index 25524fbbf3d8860cb5a5682c7f7cae4eff3f0eac..1edaa305f063773d3b14c0c81d7cb8cd6063c7ac 100644 (file)
@@ -1,5 +1,8 @@
 use std::collections::HashMap;
 
+use semver::VersionReq;
+use url::Url;
+
 use core::{Source, SourceId, SourceMap, Summary, Dependency, PackageId};
 use core::PackageSet;
 use util::{Config, profile};
@@ -77,6 +80,7 @@ pub struct PackageRegistry<'cfg> {
 
     locked: LockedMap,
     source_config: SourceConfigMap<'cfg>,
+    patches: HashMap<Url, Vec<Summary>>,
 }
 
 type LockedMap = HashMap<SourceId, HashMap<String, Vec<(PackageId, Vec<PackageId>)>>>;
@@ -97,6 +101,7 @@ impl<'cfg> PackageRegistry<'cfg> {
             overrides: Vec::new(),
             source_config: source_config,
             locked: HashMap::new(),
+            patches: HashMap::new(),
         })
     }
 
@@ -175,6 +180,39 @@ impl<'cfg> PackageRegistry<'cfg> {
         sub_vec.push((id, deps));
     }
 
+    pub fn patch(&mut self, url: &Url, deps: &[Dependency]) -> CargoResult<()> {
+        let deps = deps.iter().map(|dep| {
+            let mut summaries = self.query_vec(dep)?.into_iter();
+            let summary = match summaries.next() {
+                Some(summary) => summary,
+                None => {
+                    bail!("patch for `{}` in `{}` did not resolve to any crates",
+                          dep.name(), url)
+                }
+            };
+            if summaries.next().is_some() {
+                bail!("patch for `{}` in `{}` resolved to more than one candidate",
+                      dep.name(), url)
+            }
+            if summary.package_id().source_id().url() == url {
+                bail!("patch for `{}` in `{}` points to the same source, but \
+                       patches must point to different sources",
+                      dep.name(), url);
+            }
+            Ok(summary)
+        }).collect::<CargoResult<Vec<_>>>().chain_err(|| {
+            format!("failed to resolve patches for `{}`", url)
+        })?;
+
+        self.patches.insert(url.clone(), deps);
+
+        Ok(())
+    }
+
+    pub fn patches(&self) -> &HashMap<Url, Vec<Summary>> {
+        &self.patches
+    }
+
     fn load(&mut self, source_id: &SourceId, kind: Kind) -> CargoResult<()> {
         (|| {
             let source = self.source_config.load(source_id)?;
@@ -222,7 +260,7 @@ impl<'cfg> PackageRegistry<'cfg> {
     /// possible. If we're unable to map a dependency though, we just pass it on
     /// through.
     pub fn lock(&self, summary: Summary) -> Summary {
-        lock(&self.locked, summary)
+        lock(&self.locked, &self.patches, summary)
     }
 
     fn warn_bad_override(&self,
@@ -274,39 +312,97 @@ impl<'cfg> Registry for PackageRegistry<'cfg> {
     fn query(&mut self,
              dep: &Dependency,
              f: &mut FnMut(Summary)) -> CargoResult<()> {
-        // Ensure the requested source_id is loaded
-        self.ensure_loaded(dep.source_id(), Kind::Normal).chain_err(|| {
-            format!("failed to load source for a dependency \
-                     on `{}`", dep.name())
-        })?;
-
-
         let (override_summary, n, to_warn) = {
             // Look for an override and get ready to query the real source.
             let override_summary = self.query_overrides(&dep)?;
-            let source = self.sources.get_mut(dep.source_id());
-            match (override_summary, source) {
-                (Some(_), None) => bail!("override found but no real ones"),
-                (None, None) => return Ok(()),
-
-                // If we don't have an override then we just ship everything
-                // upstairs after locking the summary
-                (None, Some(source)) => {
-                    let locked = &self.locked;
-                    return source.query(dep, &mut |summary| f(lock(locked, summary)))
+
+            // Next up on our list of candidates is to check the `[patch]`
+            // section of the manifest. Here we look through all patches
+            // relevant to the source that `dep` points to, and then we match
+            // name/version. Note that we don't use `dep.matches(..)` because
+            // the patches, by definition, come from a different source.
+            // This means that `dep.matches(..)` will always return false, when
+            // what we really care about is the name/version match.
+            let mut patches = Vec::<Summary>::new();
+            if let Some(extra) = self.patches.get(dep.source_id().url()) {
+                patches.extend(extra.iter().filter(|s| {
+                    dep.matches_ignoring_source(s)
+                }).cloned());
+            }
+
+            // A crucial feature of the `[patch]` feature is that we *don't*
+            // query the actual registry if we have a "locked" dependency. A
+            // locked dep basically just means a version constraint of `=a.b.c`,
+            // and because patches take priority over the actual source then if
+            // we have a candidate we're done.
+            if patches.len() == 1 && dep.is_locked() {
+                let patch = patches.remove(0);
+                match override_summary {
+                    Some(summary) => (summary, 1, Some(patch)),
+                    None => {
+                        f(patch);
+                        return Ok(())
+                    }
                 }
+            } else {
+                if patches.len() > 0 {
+                    debug!("found {} patches with an unlocked dep, \
+                            looking at sources", patches.len());
+                }
+
+                // Ensure the requested source_id is loaded
+                self.ensure_loaded(dep.source_id(), Kind::Normal).chain_err(|| {
+                    format!("failed to load source for a dependency \
+                             on `{}`", dep.name())
+                })?;
+
+                let source = self.sources.get_mut(dep.source_id());
+                match (override_summary, source) {
+                    (Some(_), None) => bail!("override found but no real ones"),
+                    (None, None) => return Ok(()),
+
+                    // If we don't have an override then we just ship
+                    // everything upstairs after locking the summary
+                    (None, Some(source)) => {
+                        for patch in patches.iter() {
+                            f(patch.clone());
+                        }
+
+                        // Our sources shouldn't ever come back to us with two
+                        // summaries that have the same version. We could,
+                        // however, have an `[patch]` section which is in use
+                        // to override a version in the registry. This means
+                        // that if our `summary` in this loop has the same
+                        // version as something in `patches` that we've
+                        // already selected, then we skip this `summary`.
+                        let locked = &self.locked;
+                        let all_patches = &self.patches;
+                        return source.query(dep, &mut |summary| {
+                            for patch in patches.iter() {
+                                let patch = patch.package_id().version();
+                                if summary.package_id().version() == patch {
+                                    return
+                                }
+                            }
+                            f(lock(locked, all_patches, summary))
+                        })
+                    }
 
-                // If we have an override summary then we query the source to sanity
-                // check its results. We don't actually use any of the summaries it
-                // gives us though.
-                (Some(override_summary), Some(source)) => {
-                    let mut n = 0;
-                    let mut to_warn = None;
-                    source.query(dep, &mut |summary| {
-                        n += 1;
-                        to_warn = Some(summary);
-                    })?;
-                    (override_summary, n, to_warn)
+                    // If we have an override summary then we query the source
+                    // to sanity check its results. We don't actually use any of
+                    // the summaries it gives us though.
+                    (Some(override_summary), Some(source)) => {
+                        if patches.len() > 0 {
+                            bail!("found patches and a path override")
+                        }
+                        let mut n = 0;
+                        let mut to_warn = None;
+                        source.query(dep, &mut |summary| {
+                            n += 1;
+                            to_warn = Some(summary);
+                        })?;
+                        (override_summary, n, to_warn)
+                    }
                 }
             }
         };
@@ -321,7 +417,9 @@ impl<'cfg> Registry for PackageRegistry<'cfg> {
     }
 }
 
-fn lock(locked: &LockedMap, summary: Summary) -> Summary {
+fn lock(locked: &LockedMap,
+        patches: &HashMap<Url, Vec<Summary>>,
+        summary: Summary) -> Summary {
     let pair = locked.get(summary.source_id()).and_then(|map| {
         map.get(summary.name())
     }).and_then(|vec| {
@@ -335,7 +433,7 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary {
         Some(&(ref precise, _)) => summary.override_id(precise.clone()),
         None => summary,
     };
-    summary.map_dependencies(|mut dep| {
+    summary.map_dependencies(|dep| {
         trace!("\t{}/{}/{}", dep.name(), dep.version_req(),
                dep.source_id());
 
@@ -362,6 +460,7 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary {
             let locked = locked_deps.iter().find(|id| dep.matches_id(id));
             if let Some(locked) = locked {
                 trace!("\tfirst hit on {}", locked);
+                let mut dep = dep.clone();
                 dep.lock_to(locked);
                 return dep
             }
@@ -375,17 +474,43 @@ fn lock(locked: &LockedMap, summary: Summary) -> Summary {
         }).and_then(|vec| {
             vec.iter().find(|&&(ref id, _)| dep.matches_id(id))
         });
-        match v {
-            Some(&(ref id, _)) => {
-                trace!("\tsecond hit on {}", id);
-                dep.lock_to(id);
+        if let Some(&(ref id, _)) = v {
+            trace!("\tsecond hit on {}", id);
+            let mut dep = dep.clone();
+            dep.lock_to(id);
+            return dep
+        }
+
+        // Finally we check to see if any registered patches correspond to
+        // this dependency.
+        let v = patches.get(dep.source_id().url()).map(|vec| {
+            let dep2 = dep.clone();
+            let mut iter = vec.iter().filter(move |s| {
+                dep2.name() == s.package_id().name() &&
+                    dep2.version_req().matches(s.package_id().version())
+            });
+            (iter.next(), iter)
+        });
+        if let Some((Some(summary), mut remaining)) = v {
+            assert!(remaining.next().is_none());
+            let patch_source = summary.package_id().source_id();
+            let patch_locked = locked.get(patch_source).and_then(|m| {
+                m.get(summary.package_id().name())
+            }).map(|list| {
+                list.iter().any(|&(ref id, _)| id == summary.package_id())
+            }).unwrap_or(false);
+
+            if patch_locked {
+                trace!("\tthird hit on {}", summary.package_id());
+                let req = VersionReq::exact(summary.package_id().version());
+                let mut dep = dep.clone();
+                dep.set_version_req(req);
                 return dep
             }
-            None => {
-                trace!("\tremaining unlocked");
-                dep
-            }
         }
+
+        trace!("\tnope, unlocked");
+        return dep
     })
 }
 
index 3a93c204706fa947b1dd3ab0f9d7e772c3ca0278..4ca01c49a3178434c9408261ccda17ec74d4417b 100644 (file)
@@ -17,6 +17,14 @@ pub struct EncodableResolve {
     /// `root` is optional to allow forward compatibility.
     root: Option<EncodableDependency>,
     metadata: Option<Metadata>,
+
+    #[serde(default, skip_serializing_if = "Patch::is_empty")]
+    patch: Patch,
+}
+
+#[derive(Serialize, Deserialize, Debug, Default)]
+struct Patch {
+    unused: Vec<EncodableDependency>,
 }
 
 pub type Metadata = BTreeMap<String, String>;
@@ -153,6 +161,15 @@ impl EncodableResolve {
             metadata.remove(&k);
         }
 
+        let mut unused_patches = Vec::new();
+        for pkg in self.patch.unused {
+            let id = match pkg.source.as_ref().or(path_deps.get(&pkg.name)) {
+                Some(src) => PackageId::new(&pkg.name, &pkg.version, src)?,
+                None => continue,
+            };
+            unused_patches.push(id);
+        }
+
         Ok(Resolve {
             graph: g,
             empty_features: HashSet::new(),
@@ -160,6 +177,7 @@ impl EncodableResolve {
             replacements: replacements,
             checksums: checksums,
             metadata: metadata,
+            unused_patches: unused_patches,
         })
     }
 }
@@ -190,10 +208,12 @@ fn build_path_deps(ws: &Workspace) -> HashMap<String, SourceId> {
              config: &Config,
              ret: &mut HashMap<String, SourceId>,
              visited: &mut HashSet<SourceId>) {
-        let replace = pkg.manifest().replace();
+        let replace = pkg.manifest().replace().iter().map(|p| &p.1);
+        let patch = pkg.manifest().patch().values().flat_map(|v| v);
         let deps = pkg.dependencies()
                       .iter()
-                      .chain(replace.iter().map(|p| &p.1))
+                      .chain(replace)
+                      .chain(patch)
                       .map(|d| d.source_id())
                       .filter(|id| !visited.contains(id) && id.is_path())
                       .filter_map(|id| id.url().to_file_path().ok())
@@ -209,6 +229,12 @@ fn build_path_deps(ws: &Workspace) -> HashMap<String, SourceId> {
     }
 }
 
+impl Patch {
+    fn is_empty(&self) -> bool {
+        self.unused.is_empty()
+    }
+}
+
 #[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)]
 pub struct EncodableDependency {
     name: String,
@@ -325,10 +351,23 @@ impl<'a, 'cfg> ser::Serialize for WorkspaceResolve<'a, 'cfg> {
             Some(root) if self.use_root_key => Some(encodable_resolve_node(&root, self.resolve)),
             _ => None,
         };
+
+        let patch = Patch {
+            unused: self.resolve.unused_patches().iter().map(|id| {
+                EncodableDependency {
+                    name: id.name().to_string(),
+                    version: id.version().to_string(),
+                    source: encode_source(id.source_id()),
+                    dependencies: None,
+                    replace: None,
+                }
+            }).collect(),
+        };
         EncodableResolve {
             package: Some(encodable),
             root: root,
             metadata: metadata,
+            patch: patch,
         }.serialize(s)
     }
 }
@@ -349,30 +388,27 @@ fn encodable_resolve_node(id: &PackageId, resolve: &Resolve)
         }
     };
 
-    let source = if id.source_id().is_path() {
-        None
-    } else {
-        Some(id.source_id().clone())
-    };
-
     EncodableDependency {
         name: id.name().to_string(),
         version: id.version().to_string(),
-        source: source,
+        source: encode_source(id.source_id()),
         dependencies: deps,
         replace: replace,
     }
 }
 
 fn encodable_package_id(id: &PackageId) -> EncodablePackageId {
-    let source = if id.source_id().is_path() {
-        None
-    } else {
-        Some(id.source_id().with_precise(None))
-    };
     EncodablePackageId {
         name: id.name().to_string(),
         version: id.version().to_string(),
-        source: source,
+        source: encode_source(id.source_id()).map(|s| s.with_precise(None)),
+    }
+}
+
+fn encode_source(id: &SourceId) -> Option<SourceId> {
+    if id.is_path() {
+        None
+    } else {
+        Some(id.clone())
     }
 }
index fba3384c284c5aa975a137d8d55e9a388867bc6d..145ef6ea6cbf51785fffbcf17ca7f4b2ee1193a7 100644 (file)
@@ -53,6 +53,7 @@ use std::ops::Range;
 use std::rc::Rc;
 
 use semver;
+use url::Url;
 
 use core::{PackageId, Registry, SourceId, Summary, Dependency};
 use core::PackageIdSpec;
@@ -78,6 +79,7 @@ pub struct Resolve {
     features: HashMap<PackageId, HashSet<String>>,
     checksums: HashMap<PackageId, Option<String>>,
     metadata: Metadata,
+    unused_patches: Vec<PackageId>,
 }
 
 pub struct Deps<'a> {
@@ -111,6 +113,16 @@ struct Candidate {
 }
 
 impl Resolve {
+    pub fn register_used_patches(&mut self,
+                                 patches: &HashMap<Url, Vec<Summary>>) {
+        for summary in patches.values().flat_map(|v| v) {
+            if self.iter().any(|id| id == summary.package_id()) {
+                continue
+            }
+            self.unused_patches.push(summary.package_id().clone());
+        }
+    }
+
     pub fn merge_from(&mut self, previous: &Resolve) -> CargoResult<()> {
         // Given a previous instance of resolve, it should be forbidden to ever
         // have a checksums which *differ*. If the same package id has differing
@@ -224,6 +236,10 @@ unable to verify that `{0}` is the same as when the lockfile was generated
     pub fn query(&self, spec: &str) -> CargoResult<&PackageId> {
         PackageIdSpec::query_str(spec, self.iter())
     }
+
+    pub fn unused_patches(&self) -> &[PackageId] {
+        &self.unused_patches
+    }
 }
 
 impl fmt::Debug for Resolve {
@@ -341,6 +357,7 @@ pub fn resolve(summaries: &[(Summary, Method)],
         features: cx.resolve_features.iter().map(|(k, v)| {
             (k.clone(), v.clone())
         }).collect(),
+        unused_patches: Vec::new(),
     };
 
     for summary in cx.activations.values()
index a953f72c4d2a9db8eeb9b09238a67e515b4e2f0f..5f93baa856b6d1a7c8cccccabe4c8651380e004f 100644 (file)
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
 use std::slice;
 
 use glob::glob;
+use url::Url;
 
 use core::{Package, VirtualManifest, EitherManifest, SourceId};
 use core::{PackageIdSpec, Dependency, Profile, Profiles};
@@ -222,6 +223,20 @@ impl<'cfg> Workspace<'cfg> {
         }
     }
 
+    /// Returns the root [patch] section of this workspace.
+    ///
+    /// This may be from a virtual crate or an actual crate.
+    pub fn root_patch(&self) -> &HashMap<Url, Vec<Dependency>> {
+        let path = match self.root_manifest {
+            Some(ref p) => p,
+            None => &self.current_manifest,
+        };
+        match *self.packages.get(path) {
+            MaybePackage::Package(ref p) => p.manifest().patch(),
+            MaybePackage::Virtual(ref v) => v.patch(),
+        }
+    }
+
     /// Returns an iterator over all packages in this workspace
     pub fn members<'a>(&'a self) -> Members<'a, 'cfg> {
         Members {
index 1138be745e1292c06f3fea4cafe4557f53f90869..53374f851821ec04411973bfa4194672fb0988cf 100644 (file)
@@ -72,6 +72,15 @@ pub fn write_pkg_lockfile(ws: &Workspace, resolve: &Resolve) -> CargoResult<()>
         emit_package(dep, &mut out);
     }
 
+    if let Some(patch) = toml.get("patch") {
+        let list = patch["unused"].as_array().unwrap();
+        for entry in list {
+            out.push_str("[[patch.unused]]\n");
+            emit_package(entry.as_table().unwrap(), &mut out);
+            out.push_str("\n");
+        }
+    }
+
     if let Some(meta) = toml.get("metadata") {
         out.push_str("[metadata]\n");
         out.push_str(&meta.to_string());
index 42ecdf6bf089f49cf9cd07c56783fecca80ab127..ddbf99896b1eb9a74cca2fe99eb741c35ccb0224 100644 (file)
@@ -44,10 +44,13 @@ pub fn publish(ws: &Workspace, opts: &PublishOpts) -> CargoResult<()> {
         bail!("some crates cannot be published.\n\
                `{}` is marked as unpublishable", pkg.name());
     }
+    if pkg.manifest().patch().len() > 0 {
+        bail!("published crates cannot contain [patch] sections");
+    }
 
     let (mut registry, reg_id) = registry(opts.config,
-                                               opts.token.clone(),
-                                               opts.index.clone())?;
+                                          opts.token.clone(),
+                                          opts.index.clone())?;
     verify_dependencies(pkg, &reg_id)?;
 
     // Prepare a tarball, with a non-surpressable warning if metadata
index 8a84071b42af1b06ae70a63226762bae31f6abfc..d4387e49a381fcab9623c7912773cd0136a7c7d7 100644 (file)
@@ -130,6 +130,13 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry,
                                         .filter(|s| !s.is_registry()));
     }
 
+    let ref keep = |p: &&'a PackageId| {
+        !to_avoid_sources.contains(&p.source_id()) && match to_avoid {
+            Some(set) => !set.contains(p),
+            None => true,
+        }
+    };
+
     // In the case where a previous instance of resolve is available, we
     // want to lock as many packages as possible to the previous version
     // without disturbing the graph structure. To this end we perform
@@ -153,14 +160,37 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry,
     //    still matches the locked version.
     if let Some(r) = previous {
         trace!("previous: {:?}", r);
-        for node in r.iter().filter(|p| keep(p, to_avoid, &to_avoid_sources)) {
+        for node in r.iter().filter(keep) {
             let deps = r.deps_not_replaced(node)
-                        .filter(|p| keep(p, to_avoid, &to_avoid_sources))
+                        .filter(keep)
                         .cloned().collect();
             registry.register_lock(node.clone(), deps);
         }
     }
 
+    for (url, patches) in ws.root_patch() {
+        let previous = match previous {
+            Some(r) => r,
+            None => {
+                registry.patch(url, patches)?;
+                continue
+            }
+        };
+        let patches = patches.iter().map(|dep| {
+            let unused = previous.unused_patches();
+            let candidates = previous.iter().chain(unused);
+            match candidates.filter(keep).find(|id| dep.matches_id(id)) {
+                Some(id) => {
+                    let mut dep = dep.clone();
+                    dep.lock_to(id);
+                    dep
+                }
+                None => dep.clone(),
+            }
+        }).collect::<Vec<_>>();
+        registry.patch(url, &patches)?;
+    }
+
     let mut summaries = Vec::new();
     for member in ws.members() {
         registry.add_sources(&[member.package_id().source_id().clone()])?;
@@ -214,9 +244,7 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry,
         Some(r) => {
             root_replace.iter().map(|&(ref spec, ref dep)| {
                 for (key, val) in r.replacements().iter() {
-                    if spec.matches(key) &&
-                       dep.matches_id(val) &&
-                       keep(&val, to_avoid, &to_avoid_sources) {
+                    if spec.matches(key) && dep.matches_id(val) && keep(&val) {
                         let mut dep = dep.clone();
                         dep.lock_to(val);
                         return (spec.clone(), dep)
@@ -228,21 +256,14 @@ pub fn resolve_with_previous<'a>(registry: &mut PackageRegistry,
         None => root_replace.to_vec(),
     };
 
-    let mut resolved = resolver::resolve(&summaries, &replace, registry)?;
+    let mut resolved = resolver::resolve(&summaries,
+                                         &replace,
+                                         registry)?;
+    resolved.register_used_patches(registry.patches());
     if let Some(previous) = previous {
         resolved.merge_from(previous)?;
     }
     return Ok(resolved);
-
-    fn keep<'a>(p: &&'a PackageId,
-                to_avoid_packages: Option<&HashSet<&'a PackageId>>,
-                to_avoid_sources: &HashSet<&'a SourceId>)
-                -> bool {
-        !to_avoid_sources.contains(&p.source_id()) && match to_avoid_packages {
-            Some(set) => !set.contains(p),
-            None => true,
-        }
-    }
 }
 
 /// Read the `paths` configuration variable to discover all path overrides that
index 78b86ec91f60541a054ba80d050a9b29753a8d12..84eb68f6514178d0a6890e8d5a5a9d84e9626bf3 100644 (file)
@@ -5,11 +5,12 @@ use std::path::{Path, PathBuf};
 use std::rc::Rc;
 use std::str;
 
-use toml;
 use semver::{self, VersionReq};
 use serde::ser;
 use serde::de::{self, Deserialize};
 use serde_ignored;
+use toml;
+use url::Url;
 
 use core::{SourceId, Profiles, PackageIdSpec, GitReference, WorkspaceConfig};
 use core::{Summary, Manifest, Target, Dependency, PackageId};
@@ -218,6 +219,7 @@ pub struct TomlManifest {
     features: Option<HashMap<String, Vec<String>>>,
     target: Option<HashMap<String, TomlPlatform>>,
     replace: Option<HashMap<String, TomlDependency>>,
+    patch: Option<HashMap<String, HashMap<String, TomlDependency>>>,
     workspace: Option<TomlWorkspace>,
     badges: Option<HashMap<String, HashMap<String, String>>>,
 }
@@ -463,6 +465,7 @@ impl TomlManifest {
                 }).collect()
             }),
             replace: None,
+            patch: None,
             workspace: None,
             badges: self.badges.clone(),
         };
@@ -530,6 +533,7 @@ impl TomlManifest {
 
         let mut deps = Vec::new();
         let replace;
+        let patch;
 
         {
 
@@ -585,6 +589,7 @@ impl TomlManifest {
             }
 
             replace = me.replace(&mut cx)?;
+            patch = me.patch(&mut cx)?;
         }
 
         {
@@ -646,6 +651,7 @@ impl TomlManifest {
                                          profiles,
                                          publish,
                                          replace,
+                                         patch,
                                          workspace_config,
                                          me.clone());
         if project.license_file.is_some() && project.license.is_some() {
@@ -689,16 +695,19 @@ impl TomlManifest {
         let mut nested_paths = Vec::new();
         let mut warnings = Vec::new();
         let mut deps = Vec::new();
-        let replace = me.replace(&mut Context {
-            pkgid: None,
-            deps: &mut deps,
-            source_id: source_id,
-            nested_paths: &mut nested_paths,
-            config: config,
-            warnings: &mut warnings,
-            platform: None,
-            root: root
-        })?;
+        let (replace, patch) = {
+            let mut cx = Context {
+                pkgid: None,
+                deps: &mut deps,
+                source_id: source_id,
+                nested_paths: &mut nested_paths,
+                config: config,
+                warnings: &mut warnings,
+                platform: None,
+                root: root
+            };
+            (me.replace(&mut cx)?, me.patch(&mut cx)?)
+        };
         let profiles = build_profiles(&me.profile);
         let workspace_config = match me.workspace {
             Some(ref config) => {
@@ -711,11 +720,14 @@ impl TomlManifest {
                 bail!("virtual manifests must be configured with [workspace]");
             }
         };
-        Ok((VirtualManifest::new(replace, workspace_config, profiles), nested_paths))
+        Ok((VirtualManifest::new(replace, patch, workspace_config, profiles), nested_paths))
     }
 
     fn replace(&self, cx: &mut Context)
                -> CargoResult<Vec<(PackageIdSpec, Dependency)>> {
+        if self.patch.is_some() && self.replace.is_some() {
+            bail!("cannot specify both [replace] and [patch]");
+        }
         let mut replace = Vec::new();
         for (spec, replacement) in self.replace.iter().flat_map(|x| x) {
             let mut spec = PackageIdSpec::parse(spec).chain_err(|| {
@@ -750,6 +762,21 @@ impl TomlManifest {
         Ok(replace)
     }
 
+    fn patch(&self, cx: &mut Context)
+             -> CargoResult<HashMap<Url, Vec<Dependency>>> {
+        let mut patch = HashMap::new();
+        for (url, deps) in self.patch.iter().flat_map(|x| x) {
+            let url = match &url[..] {
+                "crates-io" => CRATES_IO.parse().unwrap(),
+                _ => url.to_url()?,
+            };
+            patch.insert(url, deps.iter().map(|(name, dep)| {
+                dep.to_dependency(name, cx, None)
+            }).collect::<CargoResult<Vec<_>>>()?);
+        }
+        Ok(patch)
+    }
+
     fn maybe_custom_build(&self,
                           build: &Option<StringOrBool>,
                           package_root: &Path)
index 4a5501f5126472da9a960927fd62311e83350f11..fd344f6db2b0c907962f46c5da5fab48963d174a 100644 (file)
@@ -424,8 +424,8 @@ properties:
   root crate's `Cargo.toml`.
 * The lock file for all crates in the workspace resides next to the root crate's
   `Cargo.toml`.
-* The `[replace]` section in `Cargo.toml` is only recognized at the workspace
-  root crate, it's ignored in member crates' manifests.
+* The `[patch]` and `[replace]` sections in `Cargo.toml` are only recognized
+  at the workspace root crate, they are ignored in member crates' manifests.
 
 [RFC 1525]: https://github.com/rust-lang/rfcs/blob/master/text/1525-cargo-workspace.md
 
@@ -629,6 +629,42 @@ includes them.
 You can read more about the different crate types in the
 [Rust Reference Manual](https://doc.rust-lang.org/reference/linkage.html)
 
+# The `[patch]` Section
+
+This section of Cargo.toml can be used to [override dependencies][replace] with
+other copies. The syntax is similar to the `[dependencies]` section:
+
+```toml
+[patch.crates-io]
+foo = { git = 'https://github.com/example/foo' }
+bar = { path = 'my/local/bar' }
+```
+
+The `[patch]` table is made of dependency-like sub-tables. Each key after
+`[patch]` is a URL of the source that's being patched, or `crates-io` if
+you're modifying the https://crates.io registry. In the example above
+`crates-io` could be replaced with a git URL such as
+`https://github.com/rust-lang-nursery/log`.
+
+Each entry in these tables is a normal dependency specification, the same as
+found in the `[dependencies]` section of the manifest. The dependencies listed
+in the `[patch]` section are resolved and used to patch the source at the
+URL specified. The above manifest snippet patches the `crates-io` source (e.g.
+crates.io itself) with the `foo` crate and `bar` crate.
+
+Sources can be patched with versions of crates that do not exist, and they can
+also be patched with versions of crates that already exist. If a source is
+patched with a crate version that already exists in the source, then the
+source's original crate is replaced.
+
+More information about overriding dependencies can be found in the [overriding
+dependencies][replace] section of the documentation and [RFC 1969] for the
+technical specification of this feature. Note that the `[patch]` feature will
+first become available in Rust 1.20, set to be released on 2017-08-31.
+
+[RFC 1969]: https://github.com/rust-lang/rfcs/pull/1969
+[replace]: specifying-dependencies.html#overriding-dependencies
+
 # The `[replace]` Section
 
 This section of Cargo.toml can be used to [override dependencies][replace] with
@@ -650,5 +686,3 @@ source (e.g. git or a local path).
 
 More information about overriding dependencies can be found in the [overriding
 dependencies][replace] section of the documentation.
-
-[replace]: specifying-dependencies.html#overriding-dependencies
index 248d4fe024fd675fa608b970c593954fffd51f5f..a4b0c342e34d1fe3d22a5ab589f893fc681cb927 100644 (file)
@@ -165,139 +165,251 @@ hello_utils = { path = "hello_utils", version = "0.1.0" }
 
 # Overriding dependencies
 
-Sometimes you may want to override one of Cargo’s dependencies. For example
-let's say you're working on a project using the
-[`uuid`](https://crates.io/crates/uuid) crate which depends on
-[`rand`](https://crates.io/crates/rand). You've discovered there's a bug in
-`rand`, however, and it's already fixed upstream but hasn't been published yet.
-You'd like to test out the fix, so let's first take a look at what your
-`Cargo.toml` will look like:
+There are a number of methods in Cargo to support overriding dependencies and
+otherwise controlling the dependency graph. These options are typically, though,
+only available at the workspace level and aren't propagated through
+dependencies. In other words, "applications" have the ability to override
+dependencies but "libraries" do not.
+
+The desire to override a dependency or otherwise alter some dependencies can
+arise through a number of scenarios. Most of them, however, boil down to the
+ability to to work with a crate before it's been published to crates.io. For
+example:
+
+* A crate you're working on is also used in a much larger application you're
+  working on, and you'd like to test a bug fix to the library inside of the
+  larger application.
+* An upstream crate you don't work on has a new feature or a bug fix on the
+  master branch of its git repository which you'd like to test out.
+* You're about to publish a new major version of your crate, but you'd like to
+  do integration testing across an entire project to ensure the new major
+  version works.
+* You've submitted a fix to an upstream crate for a bug you found, but you'd
+  like to immediately have your application start depending on the fixed version
+  of the crate to avoid blocking on the bug fix getting merged.
+
+These scenarios are currently all solved with the [`[patch]` manifest
+section][patch-section]. Note that the `[patch]` feature is not yet currently
+stable and will be released on 2017-08-31. Historically some of these scenarios
+have been solved with [the `[replace]` section][replace-section], but we'll
+document the `[patch]` section here.
+
+[patch-section]: manifest.html#the-patch-section
+[replace-section]: manifest.html#the-replace-section
+
+### Testing a bugfix
+
+Let's say you're working with the [`uuid`] crate but while you're working on it
+you discover a bug. You are, however, quite enterprising so you decide to also
+try out to fix the bug! Originally your manifest will look like:
+
+[`uuid`](https://crates.io/crates/uuid)
 
 ```toml
 [package]
-name = "my-awesome-crate"
-version = "0.2.0"
-authors = ["The Rust Project Developers"]
+name = "my-library"
+version = "0.1.0"
+authors = ["..."]
 
 [dependencies]
-uuid = "0.2"
+uuid = "1.0"
 ```
 
-To override the `rand` dependency of `uuid`, we'll leverage the [`[replace]`
-section][replace-section] of `Cargo.toml` by appending this to the end:
+First thing we'll do is to clone the [`uuid` repository][uuid-repository]
+locally via:
 
-[replace-section]: manifest.html#the-replace-section
+```shell
+$ git clone https://github.com/rust-lang-nursery/uuid
+```
+
+Next we'll edit the manifest of `my-library` to contain:
 
 ```toml
-[replace]
-"rand:0.3.14" = { git = 'https://github.com/rust-lang-nursery/rand' }
+[patch.crates-io]
+uuid = { path = "../path/to/uuid" }
 ```
 
-This indicates that the version of `rand` we're currently using, 0.3.14, will be
-replaced with the master branch of `rand` on GitHub. Next time when you execute
-`cargo build` Cargo will take care of checking out this repository and hooking
-the `uuid` crate up to the new version.
-
-Note that a restriction of `[replace]`, however, is that the replaced crate must
-not only have the same name but also the same version. This means that if the
-`master` branch of `rand` has migrated to, for example, 0.4.3, you'll need to
-follow a few extra steps to test out the crate:
-
-1. Fork the upstream repository to your account
-2. Create a branch which starts from the 0.3.14 release (likely tagged as
-   0.3.14)
-3. Identify the fix of the bug at hand and cherry-pick it onto your branch
-4. Update `[replace]` to point to your git repository and branch
-
-This technique can also be useful when testing out new features for a
-dependency. Following the workflow above you can have a branch where you add
-features, and then once it's ready to go you can send a PR to the upstream
-repository. While you're waiting for the PR to get merged you can continue to
-work locally with a `[replace]`, and then once the PR is merged and published
-you can remove `[replace]` and use the newly-published version.
-
-Note: The `Cargo.lock` file will list two versions of the replaced crate: one
-for the original crate, and one for the version specified in `[replace]`.
-`cargo build -v` can verify that only one version is used in the build.
+Here we declare that we're *patching* the source `crates-io` with a new
+dependency. This will effectively add the local checked out version of `uuid` to
+the crates.io registry for our local project.
 
-### Overriding with local dependencies
+Next up we need to ensure that our lock file is updated to use this new version
+of `uuid` so our project uses the locally checked out copy instead of one from
+crates.io. The way `[patch]` works is that it'll load the dependency at
+`../path/to/uuid` and then whenever crates.io is queried for versions of `uuid`
+it'll *also* return the local version.
 
-Sometimes you're only temporarily working on a crate and you don't want to have
-to modify `Cargo.toml` like with the `[replace]` section above. For this use
-case Cargo offers a much more limited version of overrides called **path
-overrides**.
+This means that the version number of the local checkout is significant and will
+affect whether the patch is used. Our manifest declared `uuid = "1.0"` which
+means we'll only resolve to `>= 1.0.0, < 2.0.0`, and Cargo's greedy resolution
+algorithm also means that we'll resolve to the maximum version within that
+range. Typically this doesn't matter as the version of the git repository will
+already be greater or match the maximum version published on crates.io, but it's
+important to keep this in mind!
 
-Similar to before, let’s say you’re working on a project,
-[`uuid`](https://crates.io/crates/uuid), which depends on
-[`rand`](https://crates.io/crates/rand). This time you're the one who finds a
-bug in `rand`, and you want to write a patch and be able to test out your patch
-by using your version of `rand` in `uuid`. Here’s what `uuid`’s `Cargo.toml`
-looks like:
+In any case, typically all you need to do now is:
+
+```shell
+$ cargo build
+   Compiling uuid v1.0.0 (file://.../uuid)
+   Compiling my-library v0.1.0 (file://.../my-library)
+    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
+```
+
+And that's it! You're now building with the local version of `uuid` (note the
+`file://` in the build output). If you don't see the `file://` version getting
+built then you may need to run `cargo update -p uuid --precise $version` where
+`$version` is the version of the locally checked out copy of `uuid`.
+
+Once you've fixed the bug you originally found the next thing you'll want to do
+is to likely submit that as a pull request to the `uuid` crate itself. Once
+you've done this then you can also update the `[patch]` section. The listing
+inside of `[patch]` is just like the `[dependencies]` section, so once your pull
+request is merged you could change your `path` dependency to:
+
+```toml
+[patch.crates-io]
+uuid = { git = 'https://github.com/rust-lang-nursery/uuid' }
+```
+
+[uuid-repository]: https://github.com/rust-lang-nursery/uuid
+
+### Working with an unpublished minor version
+
+Let's now shift gears a bit from bug fixes to adding features. While working on
+`my-library` you discover that a whole new feature is needed in the `uuid`
+crate. You've implemented this feature, tested it locally above with `[patch]`,
+and submitted a pull request. Let's go over how you continue to use and test it
+before it's actually published.
+
+Let's also say that the current version of `uuid` on crates.io is `1.0.0`, but
+since then the master branch of the git repository has updated to `1.0.1`. This
+branch includes your new feature you submitted previously. To use this
+repository we'll edit our `Cargo.toml` to look like
 
 ```toml
 [package]
-name = "uuid"
-version = "0.2.2"
-authors = ["The Rust Project Developers"]
+name = "my-library"
+version = "0.1.0"
+authors = ["..."]
 
 [dependencies]
-rand = { version = "0.3", optional = true }
+uuid = "1.0.1"
+
+[patch.crates-io]
+uuid = { git = 'https://github.com/rust-lang-nursery/uuid' }
 ```
 
-You check out a local copy of `rand`, let’s say in your `~/src` directory:
+Note that our local dependency on `uuid` has been updated to `1.0.1` as it's
+what we'll actually require once the crate is published. This version doesn't
+exist on crates.io, though, so we provide it with the `[patch]` section of the
+manifest.
 
-```shell
-$ cd ~/src
-$ git clone https://github.com/rust-lang-nursery/rand
+Now when our library is built it'll fetch `uuid` from the git repository and
+resolve to 1.0.1 inside the repository instead of trying to download a version
+from crates.io. Once 1.0.1 is published on crates.io the `[patch]` section can
+be deleted.
+
+It's also worth nothing that `[patch]` applies *transitively*. Let's say you use
+`my-library` in a larger project, such as:
+
+```toml
+[package]
+name = "my-binary"
+version = "0.1.0"
+authors = ["..."]
+
+[dependencies]
+my-library = { git = 'https://example.com/git/my-library' }
+uuid = "1.0"
+
+[patch.crates-io]
+uuid = { git = 'https://github.com/rust-lang-nursery/uuid' }
 ```
 
-A path override is communicated to Cargo through the `.cargo/config`
-configuration mechanism. If Cargo finds this configuration when building your
-package, it will use the override on your local machine instead of the source
-specified in your `Cargo.toml`.
+Remember that `[patch]` is only applicable at the *top level* so we consumers of
+`my-library` have to repeat the `[patch]` section if necessary. Here, though,
+the new `uuid` crate applies to *both* our dependency on `uuid` and the
+`my-library -> uuid` dependency. The `uuid` crate will be resolved to one
+version for this entire crate graph, 1.0.1, and it'll be pulled from the git
+repository.
 
-Cargo looks for a directory named `.cargo` up the directory hierarchy of
-your project. If your project is in `/path/to/project/uuid`,
-it will search for a `.cargo` in:
+### Prepublishing a breaking change
 
-* `/path/to/project/uuid`
-* `/path/to/project`
-* `/path/to`
-* `/path`
-* `/`
+As a final scenario, let's take a look at working with a new major version of a
+crate, typically accompanied with breaking changes. Sticking with our previous
+crates, this means that we're going to be creating version 2.0.0 of the `uuid`
+crate. After we've submitted all changes upstream we can update our manifest for
+`my-library` to look like:
 
-This allows you to specify your overrides in a parent directory that
-includes commonly used packages that you work on locally and share them
-with all projects.
+```toml
+[dependencies]
+uuid = "2.0"
 
-To specify overrides, create a `.cargo/config` file in some ancestor of
-your project’s directory (common places to put it is in the root of
-your code directory or in your home directory).
+[patch.crates-io]
+uuid = { git = "https://github.com/rust-lang-nursery/uuid", branch = "2.0.0" }
+```
 
-Inside that file, put this:
+And that's it! Like with the previous example the 2.0.0 version doesn't actually
+exist on crates.io but we can still put it in through a git dependency through
+the usage of the `[patch]` section. As a thought exercise let's take another
+look at the `my-binary` manifest from above again as well:
 
 ```toml
-paths = ["/path/to/project/rand"]
+[package]
+name = "my-binary"
+version = "0.1.0"
+authors = ["..."]
+
+[dependencies]
+my-library = { git = 'https://example.com/git/my-library' }
+uuid = "1.0"
+
+[patch.crates-io]
+uuid = { git = 'https://github.com/rust-lang-nursery/uuid', version = '2.0.0' }
+```
+
+Note that this will actually resolve to two versions of the `uuid` crate. The
+`my-binary` crate will continue to use the 1.x.y series of the `uuid` crate but
+the `my-library` crate will use the 2.0.0 version of `uuid`. This will allow you
+to gradually roll out breaking changes to a crate through a dependency graph
+without being force to update everything all at once.
+
+### Overriding with local dependencies
+
+Sometimes you're only temporarily working on a crate and you don't want to have
+to modify `Cargo.toml` like with the `[patch]` section above. For this use
+case Cargo offers a much more limited version of overrides called **path
+overrides**.
+
+Path overrides are specified through `.cargo/config` instead of `Cargo.toml`,
+and you can find [more documentation about this configuration][config-docs].
+Inside of `.cargo/config` you'll specify a key called `paths`:
+
+[config-docs]: config.html
+
+```toml
+paths = ["/path/to/uuid"]
 ```
 
 This array should be filled with directories that contain a `Cargo.toml`. In
-this instance, we’re just adding `rand`, so it will be the only one that’s
-overridden. This path must be an absolute path.
+this instance, we’re just adding `uuid`, so it will be the only one that’s
+overridden. This path can be either absolute or relative to the directory that
+contains the `.cargo` folder.
 
-Path overrides are more restricted than the `[replace]` section, however, in
+Path overrides are more restricted than the `[patch]` section, however, in
 that they cannot change the structure of the dependency graph. When a
-replacement is applied it must be the case that the previous set of dependencies
-all match exactly and can be used for the replacement. For example this means
-that path overrides cannot be used to test out adding a dependency to a crate,
-instead `[replace]` must be used in that situation.
+path replacement is used then the previous set of dependencies
+must all match exactly to the new `Cargo.toml` specification. For example this
+means that path overrides cannot be used to test out adding a dependency to a
+crate, instead `[patch]` must be used in that situation. As a result usage of a
+path override is typically isolated to quick bug fixes rather than larger
+changes.
 
 Note: using a local configuration to override paths will only work for crates
 that have been published to [crates.io]. You cannot use this feature to tell
 Cargo how to find local unpublished crates.
 
-More information about local configuration can be found in the [configuration
-documentation](config.html).
-
 # Platform specific dependencies
 
 
index 7b4639d824cab3709196a067dd980860e78a890b..e7376b1160453512d66b9c63fed3c2d45ae005dc 100644 (file)
@@ -177,10 +177,14 @@ impl ProjectBuilder {
 
     pub fn file<B: AsRef<Path>>(mut self, path: B,
                                 body: &str) -> ProjectBuilder {
-        self.files.push(FileBuilder::new(self.root.join(path), body));
+        self._file(path.as_ref(), body);
         self
     }
 
+    fn _file(&mut self, path: &Path, body: &str) {
+        self.files.push(FileBuilder::new(self.root.join(path), body));
+    }
+
     pub fn change_file(&self, path: &str, body: &str) {
         assert!(self.is_build.get());
         FileBuilder::new(self.root.join(path), body).mk()
@@ -350,10 +354,14 @@ impl Execs {
     }
 
     pub fn with_stderr<S: ToString>(mut self, expected: S) -> Execs {
-        self.expect_stderr = Some(expected.to_string());
+        self._with_stderr(&expected);
         self
     }
 
+    fn _with_stderr(&mut self, expected: &ToString) {
+        self.expect_stderr = Some(expected.to_string());
+    }
+
     pub fn with_status(mut self, expected: i32) -> Execs {
         self.expect_exit_code = Some(expected);
         self
diff --git a/tests/patch.rs b/tests/patch.rs
new file mode 100644 (file)
index 0000000..c333d4b
--- /dev/null
@@ -0,0 +1,744 @@
+#[macro_use]
+extern crate cargotest;
+extern crate hamcrest;
+extern crate toml;
+
+use std::fs::{self, File};
+use std::io::{Read, Write};
+
+use cargotest::support::git;
+use cargotest::support::paths;
+use cargotest::support::registry::Package;
+use cargotest::support::{execs, project};
+use hamcrest::assert_that;
+
+#[test]
+fn replace() {
+    Package::new("foo", "0.1.0").publish();
+    Package::new("deep-foo", "0.1.0")
+        .file("src/lib.rs", r#"
+            extern crate foo;
+            pub fn deep() {
+                foo::foo();
+            }
+        "#)
+        .dep("foo", "0.1.0")
+        .publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+            deep-foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = "foo" }
+        "#)
+        .file("src/lib.rs", "
+            extern crate foo;
+            extern crate deep_foo;
+            pub fn bar() {
+                foo::foo();
+                deep_foo::deep();
+            }
+        ")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#"
+            pub fn foo() {}
+        "#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] deep-foo v0.1.0 ([..])
+[COMPILING] foo v0.1.0 (file://[..])
+[COMPILING] deep-foo v0.1.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+
+    assert_that(p.cargo("build"),//.env("RUST_LOG", "trace"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn nonexistent() {
+    Package::new("baz", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = "foo" }
+        "#)
+        .file("src/lib.rs", "
+            extern crate foo;
+            pub fn bar() {
+                foo::foo();
+            }
+        ")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#"
+            pub fn foo() {}
+        "#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.1.0 (file://[..])
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn patch_git() {
+    let foo = git::repo(&paths::root().join("override"))
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/lib.rs", "");
+    foo.build();
+
+    let p = project("bar")
+        .file("Cargo.toml", &format!(r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = {{ git = '{}' }}
+
+            [patch.'{0}']
+            foo = {{ path = "foo" }}
+        "#, foo.url()))
+        .file("src/lib.rs", "
+            extern crate foo;
+            pub fn bar() {
+                foo::foo();
+            }
+        ")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#"
+            pub fn foo() {}
+        "#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] git repository `file://[..]`
+[COMPILING] foo v0.1.0 (file://[..])
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn patch_to_git() {
+    let foo = git::repo(&paths::root().join("override"))
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("src/lib.rs", "pub fn foo() {}");
+    foo.build();
+
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", &format!(r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1"
+
+            [patch.crates-io]
+            foo = {{ git = '{}' }}
+        "#, foo.url()))
+        .file("src/lib.rs", "
+            extern crate foo;
+            pub fn bar() {
+                foo::foo();
+            }
+        ");
+
+    assert_that(p.cargo_process("build"),//.env("RUST_LOG", "cargo=trace"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] git repository `file://[..]`
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.1.0 (file://[..])
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn unused() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = "foo" }
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.2.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#"
+            not rust code
+        "#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] foo v0.1.0 [..]
+[COMPILING] foo v0.1.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+
+    // unused patch should be in the lock file
+    let mut lock = String::new();
+    File::open(p.root().join("Cargo.lock")).unwrap()
+        .read_to_string(&mut lock).unwrap();
+    let toml: toml::Value = toml::from_str(&lock).unwrap();
+    assert_eq!(toml["patch"]["unused"].as_array().unwrap().len(), 1);
+    assert_eq!(toml["patch"]["unused"][0]["name"].as_str(), Some("foo"));
+    assert_eq!(toml["patch"]["unused"][0]["version"].as_str(), Some("0.2.0"));
+}
+
+#[test]
+fn unused_git() {
+    Package::new("foo", "0.1.0").publish();
+
+    let foo = git::repo(&paths::root().join("override"))
+        .file("Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.2.0"
+            authors = []
+        "#)
+        .file("src/lib.rs", "");
+    foo.build();
+
+    let p = project("bar")
+        .file("Cargo.toml", &format!(r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1"
+
+            [patch.crates-io]
+            foo = {{ git = '{}' }}
+        "#, foo.url()))
+        .file("src/lib.rs", "");
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] git repository `file://[..]`
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] foo v0.1.0 [..]
+[COMPILING] foo v0.1.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn add_patch() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] foo v0.1.0 [..]
+[COMPILING] foo v0.1.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+
+    t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+    "#));
+
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("\
+[COMPILING] foo v0.1.0 (file://[..])
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn add_ignored_patch() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.1"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] foo v0.1.0 [..]
+[COMPILING] foo v0.1.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+
+    t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+    "#));
+
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("\
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("[FINISHED] [..]"));
+}
+
+#[test]
+fn new_minor() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1.0"
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.1"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.1.1 [..]
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+}
+
+#[test]
+fn transitive_new_minor() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            subdir = { path = 'subdir' }
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("subdir/Cargo.toml", r#"
+            [package]
+            name = "subdir"
+            version = "0.1.0"
+            authors = []
+
+            [dependencies]
+            foo = '0.1.0'
+        "#)
+        .file("subdir/src/lib.rs", r#""#)
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.1"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.1.1 [..]
+[COMPILING] subdir v0.1.0 [..]
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+}
+
+#[test]
+fn new_major() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.2.0"
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.2.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.2.0 [..]
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+
+    Package::new("foo", "0.2.0").publish();
+    assert_that(p.cargo("update"),
+                execs().with_status(0));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("\
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+
+    t!(t!(File::create(p.root().join("Cargo.toml"))).write_all(br#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.2.0"
+    "#));
+    assert_that(p.cargo("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[DOWNLOADING] foo v0.2.0 [..]
+[COMPILING] foo v0.2.0
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+}
+
+#[test]
+fn transitive_new_major() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            subdir = { path = 'subdir' }
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("subdir/Cargo.toml", r#"
+            [package]
+            name = "subdir"
+            version = "0.1.0"
+            authors = []
+
+            [dependencies]
+            foo = '0.2.0'
+        "#)
+        .file("subdir/src/lib.rs", r#""#)
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.2.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(0).with_stderr("\
+[UPDATING] registry `file://[..]`
+[COMPILING] foo v0.2.0 [..]
+[COMPILING] subdir v0.1.0 [..]
+[COMPILING] bar v0.0.1 (file://[..])
+[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]
+"));
+}
+
+#[test]
+fn remove_patch() {
+    Package::new("foo", "0.1.0").publish();
+    Package::new("bar", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [dependencies]
+            foo = "0.1"
+
+            [patch.crates-io]
+            foo = { path = 'foo' }
+            bar = { path = 'bar' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#)
+        .file("bar/Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("bar/src/lib.rs", r#""#);
+
+    // Generate a lock file where `bar` is unused
+    assert_that(p.cargo_process("build"), execs().with_status(0));
+    let mut lock_file1 = String::new();
+    File::open(p.root().join("Cargo.lock")).unwrap()
+        .read_to_string(&mut lock_file1).unwrap();
+
+    // Remove `bar` and generate a new lock file form the old one
+    File::create(p.root().join("Cargo.toml")).unwrap().write_all(r#"
+        [package]
+        name = "bar"
+        version = "0.0.1"
+        authors = []
+
+        [dependencies]
+        foo = "0.1"
+
+        [patch.crates-io]
+        foo = { path = 'foo' }
+    "#.as_bytes()).unwrap();
+    assert_that(p.cargo("build"), execs().with_status(0));
+    let mut lock_file2 = String::new();
+    File::open(p.root().join("Cargo.lock")).unwrap()
+        .read_to_string(&mut lock_file2).unwrap();
+
+    // Remove the lock file and build from scratch
+    fs::remove_file(p.root().join("Cargo.lock")).unwrap();
+    assert_that(p.cargo("build"), execs().with_status(0));
+    let mut lock_file3 = String::new();
+    File::open(p.root().join("Cargo.lock")).unwrap()
+        .read_to_string(&mut lock_file3).unwrap();
+
+    assert!(lock_file1.contains("bar"));
+    assert_eq!(lock_file2, lock_file3);
+    assert!(lock_file1 != lock_file2);
+}
+
+#[test]
+fn non_crates_io() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [patch.some-other-source]
+            foo = { path = 'foo' }
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(101)
+                       .with_stderr("\
+error: failed to parse manifest at `[..]`
+
+Caused by:
+  invalid url `some-other-source`: relative URL without a base
+"));
+}
+
+#[test]
+fn replace_with_crates_io() {
+    Package::new("foo", "0.1.0").publish();
+
+    let p = project("bar")
+        .file("Cargo.toml", r#"
+            [package]
+            name = "bar"
+            version = "0.0.1"
+            authors = []
+
+            [patch.crates-io]
+            foo = "0.1"
+        "#)
+        .file("src/lib.rs", "")
+        .file("foo/Cargo.toml", r#"
+            [package]
+            name = "foo"
+            version = "0.1.0"
+            authors = []
+        "#)
+        .file("foo/src/lib.rs", r#""#);
+
+    assert_that(p.cargo_process("build"),
+                execs().with_status(101)
+                       .with_stderr("\
+[UPDATING] [..]
+error: failed to resolve patches for `[..]`
+
+Caused by:
+  patch for `foo` in `[..]` points to the same source, but patches must point \
+  to different sources
+"));
+}